{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "# Optimal Transmission Switching (OTS) (OST) with PowerModels.jl\n",
    "This tutorial describes how to run the OST feature of PowerModels.jl together with pandapower.\n",
    "The OST allows to optimize the \"switching state\" of a (meshed) grid by taking lines out of service. This not exactly the\n",
    "same as optimizing the switching state provided by pandapower. In the OST case **every in service branch element** \n",
    "in the grid is taken into account in the optimization. This includes all lines and transformers. The optimization\n",
    "then chooses some lines/transformers to be taken out of service in order to minimize fuel cost (see objective on PM website).\n",
    "\n",
    "To summerize this means:\n",
    "* the switching state of the pandapower switches are **not** changed\n",
    "* all lines / transformer in service states are variables of the optimization\n",
    "* output of the optimization is a changed \"in_service\" state in the res_line / res_trafo... tables.   \n",
    "\n",
    "For more details on PowerModels OST see:\n",
    "\n",
    "https://lanl-ansi.github.io/PowerModels.jl/stable/specifications/#Optimal-Transmission-Switching-(OTS)-1 \n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Installation\n",
    "Apart from the Julia, PowerModels, Ipopt and JuMP Installation itself (see the opf_powermodels.ipynb), you need to install\n",
    "some more libraries.\n",
    "  \n",
    "The OST problem is a mixed-integer non-linear problem, which is hard to solve. To be able to solve \n",
    "these kind of problems, you need a suitable solver. Either you use commercial ones (like Knitro) or the open-source\n",
    "Juniper solver (which is partly developed by Carleton Coffrin from PowerModels itself):\n",
    "\n",
    "* Juniper: https://github.com/lanl-ansi/Juniper.jl\n",
    "\n",
    "Additionally CBC is needed:\n",
    "\n",
    "* CBC: https://projects.coin-or.org/Cbc\n",
    "* CBC Julia interface: https://github.com/JuliaOpt/Cbc.jl\n",
    "\n",
    "Note that Juniper is a heuristic based solver. Another non-heuristic option would be to use Alpine.jl:\n",
    "* Alpine: https://github.com/lanl-ansi/Alpine.jl\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Run the OTS\n",
    "To put it simple, the goal of the optimization is to find a changed in_service state for the branch elements \n",
    "(lines, transformers). Note that the OPF calculation also takes into account the voltage and line loading limits.   \n",
    "\n",
    "In order to start the optimization, we follow two steps:\n",
    "1. Load the pandapower grid data\n",
    "2. Start the optimization\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from pandapower.networks.power_system_test_cases import case5\n",
    "from pandapower.runpm import runpm_ots\n",
    "from pandapower.run import runpp\n",
    "from pandapower.create import create_poly_cost"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# here we use the simple case5 grid\n",
    "net = case5()\n",
    "\n",
    "line_status = net[\"line\"].loc[:,\"in_service\"]\n",
    "print(\"Line status prior to optimization is:\")\n",
    "print(line_status.astype(bool))\n",
    "\n",
    "# runs the powermodels.jl switch optimization\n",
    "try:\n",
    "    runpm_ots(net, delta=1e-6)\n",
    "except Exception as err:\n",
    "    print(err)\n",
    "# note that the result is taken from the res_line instead of the line table. The input DataFrame is not changed \n",
    "line_status = net[\"line\"].loc[:,\"in_service\"]\n",
    "print(\"Line status after the optimization is:\")\n",
    "print(line_status.astype(bool))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# What to do with the result\n",
    "The optimized line / trafo status can be found in the result DataFrames, e.g. net[\"res_line\"]. The result ist **not**\n",
    "automatically written to the inputs (\"line\" DataFrame). To do this you can use:\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "# Change the input data\n",
    "#net[\"line\"].loc[:,\"in_service\"].values = net[\"res_line\"].loc[:,\"in_service\"]\n",
    "#net[\"trafo\"].loc[:,\"in_service\"].values = net[\"res_trafo\"].loc[:,\"in_service\"]\n",
    "\n",
    "# optional: run a power flow calculation with the changed in service status\n",
    "runpp(net)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "If you have line-switches / trafo-switches at these lines/trafos you could also search for the switches connected to\n",
    "these elements (with the topology search) and change the switching state according to the in_service result. \n",
    "This should deliver identical results as changing the in service status of the element. \n",
    "However, this requires to have line switches at **both** ends of the line. If you just open\n",
    "the switch on one of the two sides, the power flow result is slightly different since the line loading of the\n",
    "line without any connected elements is calculated.\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Notes\n",
    "Juniper is based on a heuristic, it does not necessarily find the global optimum. For this use another solver\n",
    "\n",
    "In the PowerModels OPF formulation, generator limits, voltage limits and loading limits are taken into account. \n",
    "This means you have to specify limits for all gens, ext_grids and controllable sgens / loads. Optionally costs for these can be defined. \n",
    "Also limits for line/trafo loadings and buse voltages are to be defined. The case5 grid has pre-defined limits set. \n",
    "In other cases you might get an error. Here is a code snippet:\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "def define_ext_grid_limits(net):\n",
    "    # define line loading and bus voltage limits\n",
    "    min_vm_pu = 0.95\n",
    "    max_vm_pu = 1.05\n",
    "\n",
    "    net[\"bus\"].loc[:, \"min_vm_pu\"] = min_vm_pu\n",
    "    net[\"bus\"].loc[:, \"max_vm_pu\"] = max_vm_pu\n",
    "\n",
    "    net[\"line\"].loc[:, \"max_loading_percent\"] = 100.\n",
    "    net[\"trafo\"].loc[:, \"max_loading_percent\"] = 100.\n",
    "    \n",
    "    # define limits\n",
    "    net[\"ext_grid\"].loc[:, \"min_p_mw\"] = -9999.\n",
    "    net[\"ext_grid\"].loc[:, \"max_p_mw\"] = 9999.\n",
    "    net[\"ext_grid\"].loc[:, \"min_q_mvar\"] = -9999.\n",
    "    net[\"ext_grid\"].loc[:, \"max_q_mvar\"] = 9999.\n",
    "    # define costs\n",
    "    for i in net.ext_grid.index:\n",
    "        create_poly_cost(net, i, 'ext_grid', cp1_eur_per_mw=1)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3"
  },
  "pycharm": {
   "stem_cell": {
    "cell_type": "raw",
    "metadata": {
     "collapsed": false
    },
    "source": []
   }
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}
